package org.fhnw.aigs.server.gameHandling;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.bind.JAXB;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient;
import org.fhnw.aigs.commons.communication.IdentificationResponseMessage;
import org.fhnw.aigs.server.common.LogRouter;
import org.fhnw.aigs.server.common.LoggingLevel;
import org.fhnw.aigs.server.gui.ServerGUI;
import org.fhnw.aigs.server.common.ServerConfiguration;
/**
* This class is responsible for all User related tasks. It loads generates user
* files, loads users, logs them on and off.<br>
* v1.0 Initial release<br>
* v1.1 Major changes in handling<br>
* v1.1.1 Bugfixes<br>
* v1.2 Changing of logging
*
* @author Matthias Stöckli
* @version 1.2 (Raphael Stoeckli, 24.02.2015)
*/
@XmlRootElement(name = "User")
public class User {
/**
* A number used to generate unique user IDs. Will not be marshalled.
*/
@XmlTransient
public static long userCount;
/**
* A list of all current users. Will not be marshalled.<br>
* Do not use add and remove methods directly on this list. <br>
* Use instead {@link User#addUserToUserList(org.fhnw.aigs.server.gameHandling.User)} and
* {@link User#removeUserFromUserList(org.fhnw.aigs.server.gameHandling.User)}.
*/
@XmlTransient
public static UserList users;
/**
* The user's unique ID.
*/
private long id;
/**
* The user's name.
*/
private String userName;
/**
* The user's "password" or identification code.
*/
private String identificationCode;
/**
* A flag that indicates whether or not a user is online.
*/
private boolean loggedIn;
/**
* Indicates whether the user is a regular (known) user or an anonymos (adHoc created) user.<br>Will not be marshalled because adHoc users are not stored<br>
* See also {@link User#writeUsersToXml()} about the handling of non-persistent users
* @since v1.1
*/
@XmlTransient
private boolean anonymousUser;
/**
* Indicates whether the user is a copy of an already logged in user.<br>Will not be marshalled because doppelgangers are not stored<br>
* See also {@link User#writeUsersToXml()} about the handling of non-persistent users
* @since v1.1
*/
@XmlTransient
private boolean doppelganger;
/**
* Indicates whether the user is a AI. This parameter is <b>optional</b>. Use it if you want to show an AI user in the server GUI.<br>Will not be marshalled because AIs are not stored<br>
* See also {@link User#writeUsersToXml()} about the handling of non-persistent users
* @since v1.1
*/
@XmlTransient
private boolean aiUser;
/**
* See {@link User#id}.
*/
@XmlElement(name = "Id")
public long getId() {
return id;
}
/**
* See {@link User#userName}.
*/
@XmlElement(name = "UserName")
public String getUserName() {
return userName;
}
/**
* See {@link User#identificationCode}.
*/
@XmlElement(name = "IdentificationCode")
public String getIdentificationCode() {
return identificationCode;
}
/**
* See {@link User#loggedIn}.
*/
@XmlTransient
public boolean isLoggedIn() {
return loggedIn;
}
/**
* See {@link User#anonymousUser}.
*/
@XmlTransient
public boolean isAnonymousUser(){
return anonymousUser;
}
/**
* See {@link User#doppelganger}.
*/
@XmlTransient
public boolean isDoppelganger(){
return doppelganger;
}
/**
* See {@link User#aiUser}.
*/
@XmlTransient
public boolean isAI(){
return aiUser;
}
/**
* See {@link User#id}.
*/
public void setId(long id) {
this.id = id;
}
/**
* See {@link User#userName}.
*/
public void setUserName(String userName) {
this.userName = userName;
}
/**
* See {@link User#identificationCode}.
*/
public void setIdentificationCode(String identificationCode) {
this.identificationCode = identificationCode;
}
/**
* See {@link User#loggedIn}.
*/
public void setLoggedIn(boolean loggedIn) {
this.loggedIn = loggedIn;
}
/**
* See {@link User#anonymousUser}.
*/
public void setAnonymousUser(boolean isAnonymousUser) {
this.anonymousUser = isAnonymousUser;
}
/**
* See {@link User#doppelganger}.
*/
public void setDoppelganger(boolean isDoppelganger) {
this.doppelganger = isDoppelganger;
}
/**
* See {@link User#aiUser}.
*/
public void setAI(boolean isAI) {
this.aiUser = isAI;
}
/**
* Empty, zero-argument constructor.
*/
public User() {
}
/**
* Creates a new instance of User with a given user.
*
* @param userName The user's name.
* @param password The user's password/identification code.
*/
public User(String userName, String password) {
this.id = getNextID();//userCount++;
this.userName = userName;
this.identificationCode = password;
}
/**
* Calculates the next free user ID from the static user list
* @since v1.1
* @return Next free user ID
*/
private static long getNextID()
{
User.userCount = User.users.size();
long id = -1;
for (int i = 0; i < User.userCount; i++)
{
if (User.users.get(i).getId() > id)
{
id = User.users.get(i).getId();
}
}
id++;
return id;
}
/**
* Gets a user by his or her name.
*
* @param userName The user's name.
* @return The user with the specified name or null if there is none.
*/
public static User getUserByName(String userName) {
for (User user : users) {
if (user.userName.equals(userName)) {
return user;
}
}
return null;
}
/**
* Adds a new user to the static user list
* @param user User to add
*/
public static void addUserToUserList(User user)
{
users.add(user);
if (ServerConfiguration.getInstance().getIsAnonymousLoginAllowed() == true)
{
if (user.isNonPersistentUser() == true)
{
ServerGUI.getInstance().addUserToList(user); // Add only non-persistent users
}
}
else
{
ServerGUI.getInstance().addUserToList(user); // Add all users
}
userCount = users.size();
}
/**
* Removes a User from the static user list
* @param user User to remove
* @since v1.1
*/
public static void removeUserFromUserList(User user)
{
try
{
ServerGUI.getInstance().removeUserFromList(user);
users.remove(user);
userCount = users.size(); // Update user Count
}
catch(Exception e)
{
// No action. User is somply not in list --> Go ahead
}
}
/**
* Removes all non persistent users (anonymous/AdHoc and doppelganger) from
* the static user list
* @since v1.1
*/
public static void removeAllNonPersistentUsers()
{
for(int i = users.size() - 1; i >= 0; i--) // Inverse is better to delete the list from the end
{
if (users.get(i).isNonPersistentUser() == true)
{
removeUserFromUserList(users.get(i));
users.remove(i);
}
}
if (users.size() == 0)
{
User.userCount = 0; // Correction if <0
}
}
/**
* Method checks whether the user is non persistent:<br>
* <ul>
* <li>Anonymous user (AdHoc)</li>
* <li>Doppelganger</li>
* <li>AI User</li>
* </ul>
* @return True if the user is non-persistent, otherwise false
*/
public boolean isNonPersistentUser()
{
if (this.anonymousUser == true || this.doppelganger == true || this.isAI() == true)
{
return true;
}
else
{
return false;
}
}
/**
* Generates a password or identification code based on the specified
* length. It consists of alphebetic values (a-Z).
*
* @param length The length of the password
* @return A random password.
*/
private static String generateRandomPassword(int length) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < length; i++) {
// Get random number boolean and random number from 0 to 25.
// If the boolean is true, the letter will be a capital letter.
SecureRandom random = new SecureRandom();
boolean isCapital = random.nextBoolean();
// Get a CharacterCode (char) based on the reandom number and
// capitalize it, if the random boolean is true.
// Then append the character to the password.
int randomCharacterCode = random.nextInt(26);
char character = (char) ('a' + (char) randomCharacterCode);
if (isCapital) {
character = Character.toUpperCase(character);
}
stringBuilder.append(character);
}
return stringBuilder.toString();
}
/**
* Reads the file "conf/users.txt" and generates a new usersXml file. The
* document should be plaintext. Every line is equals to one user.
*
* @throws IOException Thrown when the file could not be read.
* @throws JAXBException Thrown when the users could not be serialized.
* @deprecated Do not use thes method anymore. Use XML serialization ({@link User#readUsersFromXml()}) instead
*/
private static void readUsersFromPlainText() throws IOException, JAXBException {
File userFile = new File("conf/users.txt");
//LOG//Logger.getLogger(User.class.getName()).log(Level.INFO, "Generating users from text file...");
LogRouter.log(User.class.getName(), LoggingLevel.info, "Generating users from text file...");
// Reads the file.
InputStreamReader inputStreamReader = new FileReader(userFile);
try (BufferedReader reader = new BufferedReader(inputStreamReader)) {
String input = "";
users = new User.UserList();
// Read file line by line
while ((input = reader.readLine()) != null) {
if (input.equals("")) {
continue; // Skip empty lines
}
// Use the current line as name and generate a new password
String name = input;
String identificationCode = generateRandomPassword(6);
// Create a new user based on the name and password/identification code
User user = new User(name, identificationCode);
User.addUserToUserList(user);
//LOG//Logger.getLogger(User.class.getName()).log(Level.INFO, "New user: {0}", name);
LogRouter.log(User.class.getName(), LoggingLevel.info, "New user: {0}", name);
}
}
// Save the file to "conf/usersXml.xml".
File outputFile = new File("conf/usersXml.xml");
JAXB.marshal(users, outputFile);
//LOG//Logger.getLogger(User.class.getName()).log(Level.INFO, "Successfully created {0} users and saved to config file.", users.size());
LogRouter.log(User.class.getName(), LoggingLevel.info, "Successfully created {0} users and saved to config file.", users.size());
}
/**
* Handles the user login. If anonymous login is allowed, an Ad-hoc user will be created. If multiple logins are allowed, a doppelganger will be created.
* @param userName The user (login) name to be checked.
* @param playerName The users' displayed name
* @param password The user's password/identification code.
* @param isMultiLoginAllowed Flag that indicates whether the configuration
* @return A IdentificationResponseMessage which is sent back to the client.
* @since v1.1
*/
public static IdentificationResponseMessage identify(String userName, String password, String playerName, boolean isMultiLoginAllowed)
{
IdentificationResponseMessage loginMessage;
if (ServerConfiguration.getInstance().getIsAnonymousLoginAllowed() == true) // No password or Username required
{
User adHoc = null;
String tempUsername = userName;
if (tempUsername.length() == 0)
{
tempUsername = "Player";
}
String tempPassword = password;
while (true) // Check until a valid user is determined
{
loginMessage = User.checkCredentials(tempUsername, tempPassword, isMultiLoginAllowed);
if (loginMessage.getLoginSuccessful() == false) // A user with these credentials already exists -> choose other name and password
{
tempUsername = getUnusedUsername(tempUsername);
tempPassword = User.generateRandomPassword(8);
adHoc = new User(tempUsername, tempPassword);
adHoc.setAnonymousUser(true);
//User.users.add(adHoc);
User.addUserToUserList(adHoc);
continue;
}
else
{
break;
}
}
loginMessage.setPassword(tempPassword);
}
else
{
loginMessage = User.checkCredentials(userName, password, isMultiLoginAllowed);
loginMessage.setPassword(password);
}
loginMessage.setPlayerName(playerName);
return loginMessage;
}
/**
* Returns an unused user name. This method is importatnt for anonymous login (Ad-hoc users)
* @param template The initial username to check
* @return A currently not used user name
* @since v1.1
*/
private static String getUnusedUsername(String template)
{
int counter = 1;
boolean match = false;
String userName = template;
while(true)
{
match = false;
for(User user : User.users)
{
if (user.getUserName().equals(userName) == true)
{
match = true;
counter++;
userName = template + Integer.toString(counter);
break;
}
}
if (match == false)
{
break;
}
}
return userName;
}
/**
* Checks whether a login attempt was successful of not and generates a
* IdentificiationResponseMessage based on the result.
*
* @param userName The user name to be checked.
* @param identificationCode The user's password/identification code.
* @param isMultiLoginAllowed Flag that indicates whether the configuration
* option {@link ServerConfiguration#isMultiLoginAllowed} is set to true or
* the server is running on localhost.
* @return A IdentificationResponseMessage which is sent back to the client.
* @since v1.1 [was originally method identify(...)]
*/
private static IdentificationResponseMessage checkCredentials(String userName, String identificationCode, boolean isMultiLoginAllowed) {
IdentificationResponseMessage identificationResponseMessage = new IdentificationResponseMessage();
// Iterate through all users
for (User user : User.users) {
// Check for correct user name and password. If the name and the
// identification code do not match, set the respective flags
// in the IdentificationResponseMessage
if (user.getUserName().equals(userName) == false || user.getIdentificationCode().equals(identificationCode) == false) {
identificationResponseMessage.setLoginSuccessful(false);
identificationResponseMessage.setReason("User name or password is incorrect.");
// Check if the user is not logged in. If so, the login attempt
// is successful.
} else {
if (user.isLoggedIn() == false) {
user.setLoggedIn(true);
identificationResponseMessage.setLoginName(user.getUserName());
identificationResponseMessage.setLoginSuccessful(true);
identificationResponseMessage.setReason("Login successful.");
break;
// If the user is already logged in, check whether multi login
// is allowed. If so, generate a "doppelganger", a copy of the
// current user with a number added to the name.
} else {
if (isMultiLoginAllowed) {
User newUser = User.generateDoppelganger(user);
newUser.setDoppelganger(true);
identificationResponseMessage.setLoginName(newUser.getUserName());
identificationResponseMessage.setLoginSuccessful(true);
identificationResponseMessage.setReason("Created doppelganger. Login successful.");
User.addUserToUserList(newUser);
break;
// If the user is already logged in and multi login is not
// allowed, set the flags accordingly.
} else {
user.setLoggedIn(true);
identificationResponseMessage.setLoginName(user.getUserName());
identificationResponseMessage.setLoginSuccessful(false);
identificationResponseMessage.setReason("User is already logged in.");
return identificationResponseMessage;
}
}
}
}
return identificationResponseMessage;
}
/**
* Remove a player from the list of active players/users. The user will not be removed
* from the static user list. Use method {@link User#removeUserFromUserList(org.fhnw.aigs.server.gameHandling.User)}
* if a user must be removed from the static user list.
*
* @param name The name of the player to be logged off.
*/
public static void logOffUserByName(String name) {
for (int i = 0; i < users.size(); i++) {
User user = users.get(i);
if (user.getUserName().equals(name)) {
users.get(i).loggedIn = false;
//LOG//Logger.getLogger(User.class.getName()).log(Level.INFO, "Marked player " + user.getUserName() + " as being offline.", users.size());
LogRouter.log(User.class.getName(), LoggingLevel.info, "Marked player " + user.getUserName() + " as being offline.", users.size());
}
}
}
/**
* Returns a list of stored users from the file conf/usersXml.xml
* @return List of registred users
*/
public static UserList readUsersFromXml() {
// Load the users config file and check for the existance
File userConfigFile = new File("conf/usersXml.xml");
if (userConfigFile.exists()) {
// Parse the user configuration file's content directly using JAXB
users = (User.UserList) JAXB.unmarshal(userConfigFile, User.UserList.class);
//LOG//Logger.getLogger(User.class.getName()).log(Level.INFO, "Read user configuration.");
LogRouter.log(User.class.getName(), LoggingLevel.system, "Read user configuration.");
} else {
//LOG//Logger.getLogger(User.class.getName()).log(Level.INFO, "User configuration file could not be read. Creating an empty user list.");
LogRouter.log(User.class.getName(), LoggingLevel.system, "User configuration file could not be read. Creating an empty user list.");
users = new UserList();
}
return users;
}
/**
* Saves the static user list to the file conf/usersXml.xml
* @since v1.1
*/
public static void writeUsersToXml(){
UserList tempList = new UserList();
for(int i = 0; i < users.size(); i++)
{
if (users.get(i).isNonPersistentUser() == true)
{
continue; // Skip non-persistent users
}
tempList.add(users.get(i));
}
File userConfigFile = new File("conf/usersXml.xml");
try
{
JAXB.marshal(tempList, userConfigFile);
//LOG//Logger.getLogger(User.class.getName()).log(Level.INFO, "Write user configuration.");
LogRouter.log(User.class.getName(), LoggingLevel.system, "Write user configuration.");
}
catch(Exception ex)
{
//LOG//Logger.getLogger(User.class.getName()).log(Level.INFO, "User configuration file could not be written.");
LogRouter.log(User.class.getName(), LoggingLevel.system, "User configuration file could not be written.");
}
}
/**
* Reads users from a text file and writes it to an xml file.
*
* @param args No need for that.
* @throws IOException
* @throws JAXBException
* @deprecated Do not use this method. Use the visual user management tool from she Server GUI instead
*/
public static void main(String args[]) throws IOException, JAXBException {
readUsersFromPlainText();
}
/**
* Generates a new user based on another user in the case multi login is
* allowed.
*
* @param loggedInUser The "original" user.
* @return The "doppelganger".
*/
public static synchronized User generateDoppelganger(User loggedInUser) {
// Create a new user with the same identification
User doppelganger = new User();
doppelganger.setId(userCount++);
doppelganger.setIdentificationCode(loggedInUser.getIdentificationCode());
// Get the old name
String newName = "";
String loggedInUserName = loggedInUser.getUserName();
ArrayList<User> existingDoppelgangers = new ArrayList<>();
for (User user : users) {
if (user.getUserName().startsWith(loggedInUserName)) {
existingDoppelgangers.add(user);
}
}
if (existingDoppelgangers.size() > 0) {
// The last doppelganger must be the newest one
User newestDoppelganger = existingDoppelgangers.get(existingDoppelgangers.size() - 1);
// Check if he or she has a number at the end of the user name
Pattern p = Pattern.compile("(\\w*\\.\\w*)(\\d)");
Matcher m = p.matcher(newestDoppelganger.getUserName());
if (m.find()) {
String lastNumber = m.group(2);
// Try to add 1 to the user name.
int parsedLastNumber = Integer.parseInt(lastNumber) + 1;
newName = m.group(1) + parsedLastNumber++;
} else {
newName = loggedInUserName + "1";
}
doppelganger.setUserName(newName);
}
return doppelganger;
}
/**
* This construct is storing users - it was created for XML-parsing purposes
* and is just a wrapper for an ArrayList which is not natively supported by
* JAXB without modifications.
* @author Matthias Stöckli
* @version v1.0
*/
@XmlRootElement(name = "Users")
@XmlSeeAlso({User.class})
public static class UserList extends ArrayList<User> {
/**
* Standard constructor
*/
public UserList()
{
}
@XmlElement(name = "User")
public List<User> getUsers()
{
return this;
}
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(this.getUserName());
sb.append(" (ID: ");
sb.append(this.getId());
sb.append(")");
if (this.isAnonymousUser() == true)
{
sb.append(" [AdHoc User]");
}
if (this.isDoppelganger() == true)
{
sb.append(" [Doppleganger]");
}
if (this.isAI() == true)
{
sb.append(" [AI]");
}
return sb.toString();
}
}